nginx + lua + webSocketでかんたんpub-subサーバ


概要

実験用に簡単に使えるWebSocketバックエンドが欲しかったので、

ngx + ngx_lua_module からのゴリ押しで作ってみた。


sassembla / nginx-luajit-websocket-pubsuber

https://github.com/sassembla/nginx-luajit-websocket-pubsuber


構成

nginx 1.7.10

ngx_lua_module 0.9.15

redis 2.8.9


luajit 2.1 alpha

websocket protocol + server

redis connector

other


動作内容

nginx-luaでは、リクエストごとに完璧にisolateされたluaスクリプト動作が発生する。


たとえばWebSocket受ける場合、

プロセスをwhileでがっちりブロックしてレスポンス返さずにWebSocket接続という形になった。


https://github.com/sassembla/nginx-luajit/blob/master/bin/lua/client.lua#L69

-- start websocket serving

while true do

local recv_data, typ, err = wb:recv_frame()


if wb.fatal then

local jsonData = json:encode({connectionId = serverId, state = STATE_DISCONNECT_1})

pubRedisCon:publish(IDENTIFIER_CENTRAL, jsonData)



で、luaのコンテキストがリクエストごとにisolateされてるとは言っても、pub-subみたいなことをしたい場合、中継がなくて困るので、

中継にredisのpubsubを使っている。


redidのpubsubにpush性能は無く、こちらも単に、特定のキーのsubscribeをwhileループで行っている。


https://github.com/sassembla/nginx-luajit/blob/master/bin/lua/client.lua#L114

-- subscribe loop

-- waiting data from central.

function subscribe ()

while true do

local res, err = subRedisCon:read_reply()

if not res then

ngx.log(ngx.ERR, "redis subscribe read error:", err)

break

else

-- for i,v in ipairs(res) do

-- ngx.log(ngx.ERR, "client i:", i, " v:", v)

-- end


もちろん一個のリクエストプロセス中で2個while書けるはずなくて、ngx_lua_moduleから使えるthread生成を頼っている。


https://github.com/sassembla/nginx-luajit/blob/master/bin/lua/client.lua#L60

function connectWebSocket()

-- start subscribe

ngx.thread.spawn(subscribe)


-- send connected

local jsonData = json:encode({connectionId = serverId, state = STATE_CONNECT})

pubRedisCon:publish(IDENTIFIER_CENTRAL, jsonData)


-- start websocket serving

while true do

local recv_data, typ, err = wb:recv_frame()



中央部分と残念なとこ

redisでpubsubを使って、client x n -> central, central -> client x n を繋いでいる。

つまりredisのpubsubが2系統ある。


で、clientはWebSocket接続なわけだが、centralは、

httpで通信してきたプロセスをredisのcentral pubsubのためにガッチリ拘束する、っていう残念な方法を取っている。


https://github.com/sassembla/nginx-luajit/blob/master/bin/lua/controlpoint.lua#L99

function main ()

-- start waiting loop

while true do

local res, err = subRedisCon:read_reply()

if not res then

ngx.log(ngx.ERR, "failed to receiving data from clients, err:", err)

ngx.exit(500)

return

else


つまり現在の構造は、以下の順でしか動作できない。

1.httpでcentralのパスにアクセス(この通信は帰ってこない)

2.適当にWebSocket接続

3.中央コンテキストでいろいろできる

httpで通信してきただけのやつを停めるのどうなの? とか、それ起動時に自動的にできないの? とか、そのへんを模索しているところ。


副産物

期せずしてredisのpub subに依存したが、これによってclient側へのpushの責務がclient connection側に一任されていて、

送付確認周りをわりと疎結合かつ非同期に作る事が出来た。


つまり一対多のWebSocketのsendにロックがかかっていない。

使っててちょっと楽だ。実用に耐えるとは思ってないけど。


また、redisでの中継でclients - centralを繋いでいるので、clientsからどのpubsubを使うか指定させる事でルームみたいな概念が実現できた。

なかなかおもしろい。


ロックせず、各自のwhileループで回っている部分自体はまあはいって感じなんだけど、redisのpubsubによってタイムラグは若干出ると思う。


client側の実装として、udp版やmqtt版みたいなのも作ってみたい。

まあキューイング部分が全部redisに寄るので、あっさり破綻すると思うけど。

中間にRabbitMQとかくっつけるのが正しい気がする。


エラー処理について

clientは、エラーが起こった瞬間subscribeをヤメて自沈する。

というかぶっちゃけredisについて詳しく知らないので、何がどうなってるのかコード追ったりしてみようと思う。


良く出来てるソフトウェアが多くて助かる。



とりあえず動かせたので

nginxのモジュール書くか、

コンセプトモデルだけこれでOKということにしてnginxを基礎にした別物をCでガンガン書くか、

何もかもなかったことにしてコンセプトをnginxから学びつつgoで遊ぶかの3択中。

ngx_lua、楽しいです。